<?php
/**
 * This file contains the BaseDAO class definition.
 * References were made to "Chapter 22: Programming the Application",
 * section "Data Access Object and User Object" in the book "Professional
 * PHP Design Patterns" by Aaron Saray, 2009, Wiley Publishing, Inc.
 */

abstract class BaseDAO {
    
    private $databaseTableName;
    protected $fetchedData = array();
    
    /**
     * Constructor.
     * Provides a convenient way to make a BaseDAO object, optionally initialised
     * with values fetched from the database.
     * 
     * @param string $databaseTableName
     * The name of the database table this BaseDAO object will be associated with.
     * 
     * @param mixed $fetchConditions
     * This is an optional argument to allow data to be fetched selectively
     * at construction, where 
     * (a) If it is a number, this constructor will
     * look for it under the 'id' column in the database.
     * (b) If it is an array, this constructor will treat it as an array of
     * conditions for the SQL statement, SELECT. Ie. given an array where
     * array[$key] = $value, the database will be queried with
     * "SELECT * from $table WHERE $key = $value".
     * 
     * @throws Exception
     * if $databaseTableName is not a string, or
     * if $fetchConditions is not of a type this constructor can
     * handle.
     */
    public function __construct($databaseTableName, $fetchConditions = NULL) {
        
        if (is_null($databaseTableName) || !is_string($databaseTableName)) {
            throw new Exception(
                    'Please specify the name of a database table.' . 
                    'You gave: ' . $databaseTableName);
        }
        
        // asssuming program flow doesn't continue to here if the exception above
        // has been triggered.
        $this->databaseTableName = $databaseTableName;
        
        if (!is_null($fetchConditions)) {
            
            $buffer = array();
            
            if (is_numeric($fetchConditions)) {
                $buffer = array('id'=>$fetchConditions);
                
            } else if (is_array($fetchConditions)) {
                $buffer = $fetchConditions;
                
            } else {
                throw new Exception('Cannot initialise with the specified value');
            }
            
            // Finally make the call to the data source (database).
            $this->fetch($buffer);
        }
    }
    
    /**
     * Method: commit
     * 
     * @param Content $content
     * The content to be committed to the database.
     * 
     */
    public function commit(&$content)  {
        if ($content->getId() == Content::ID_DOESNT_EXIST) { // TODO ensure getId() 
            $this->create($content);
        } else {
            $this->update($content);
        }
    }
    
    /**
     * Method: setValue.
     * A simple setter method for the internal state of this class.
     * 
     * @param mixed $key
     * The key for the specified value in the internal array.
     * 
     * @param mixed $value
     * The value that the specified key will hold in the array internal to this class.
     */
    protected function setValue($key, $value) {
        $this->fetchedData[$key] = $value;
    }
    
    /**
     * Method: getValue.
     * Returns the value held by the specified key in this class' internal state.
     * Returns null if the specified key does not hold a value.
     * 
     * @param mixed $key
     */
    protected function getValue($key) {
        if (!isset($this->fetchedData[$key])) {
            return null;   
        } else {
            return $this->fetchedData[$key];
        }       
    }
    
    /**
     * Method: create
     */
    protected function create(&$content) {
        
        $connection = DatabaseConnection::getInstance();
        
        // Sanitize data from user for storage in database.
        $sanitizedValues = array();
        foreach($content->getDetailsArray() as $detail) {
            $sanitizedValues[] = $connection->sanitize($detail); // Use of empty square brackets is based on PHP's "auto-increment" feature for the array index.
        }
        
        // Prepare SQL statement.
        // eg. "INSERT INTO my_table (key1, key2) VALUES ('stringValue1', 'stringValue2')".
        $statement = "INSERT INTO {$this->databaseTableName} (`";
        $statement .= implode('`, `', array_keys($content->getDetailsArray())); // TODO ensure getDetailsArray method is defined in code for Content class (or subclass).
        
        $statement .= "`) VALUES ('";
        $statement .= implode("', '", $sanitizedValues);
        $statement .= "')";
       
        // Update $content with the id returned from the database.
        // The id column shall be an AUTO_INCREMENT primary key in the table
        // specified by $this->databaseTableName.
        $content->setId($connection->getIdAfterInsert($statement));
        
        // TODO (phengheong@hotmail.com) Consider calling the fetch method here
        // to update the data that this class is holding.
    }
    
    /**
     * Method: update
     */
    protected function update(&$content) {
        
        $connection = DatabaseConnection::getInstance();
        
        // Sanitize values defined by user for storage in database.
        // At the same time, concatenate into a form suitable for SQL.
        // Eg. "UPDATE table_name SET block='428', floor='11' WHERE last_name='carrot';
        $updates = array();
        foreach($content->getDetailsArray() as $key=>$value) {
            $updates[] = "`{$key}`='" .
                    $connection->sanitize($value) .
                    "'";
        }
        
        // Prepare SQL query.
        $statement = "UPDATE {$this->databaseTableName} SET";
        $statement .= implode(',', $updates);
        $statement .= "WHERE id={$content->getId()}";
        
        $connection->execute($statement);
        
        // TODO (phengheong@hotmail.com) Consider calling the fetch method here
        // to update the data that this class is holding.
    }
    
    /**
     * Method: delete
     * 
     * @param int $contentId
     * 
     * @throws Exception
     * if there is an error in deleting from the underlying database.
     */
    protected function delete($contentId) {
        
        $connection = DatabaseConnection::getInstance();
        
        $statement = "DELETE FROM {$this->databaseTableName}" .
                " WHERE id=$contentId";
                
        if(!$connection->execute($statement)) {
            // Query has been executed and has failed.
            throw new Exception("Error in deletion: {$connection->getErrorMessage()}");
        }
        
        // TODO (phengheong@hotmail.com) Consider calling the fetch method here
        // to update the data that this class is holding.
    }
    
    /**
     * Method: deleteAll
     * 
     * @throws Exception
     * if there is an error in deleting from the underlying database.
     */
    protected function deleteAll() {
        
        $connection = DatabaseConnection::getInstance();
        
        $statement = "DELETE FROM {$this->databaseTableName}";
        
        if (!$connection->execute($statement)) {
            throw new Exception("Error in deleting all from " .
                    "{$this->databaseTableName}: {$connection->getErrorMessage()}");
        }
        
        // TODO (phengheong@hotmail.com) Consider calling the fetch method here
        // to update the data that this class is holding.
    }

    /**
     * Method: fetch
     * 
     * Executes a SELECT-type SQL query against the database, and stores
     * the resulting data if there is any.
     * 
     * @param array $fetchConditions
     * An array containing conditions for a SELECT statement in SQL.
     * Ie, "WHERE $key = value". The array shall have the form array[$key] = $value.
     */
    protected function fetch($fetchConditions){
        
        $connection = DatabaseConnection::getInstance();
        $selectStatement = "SELECT * FROM {$this->databaseTableName}";
        
        // Set up WHERE conditions, if any.
        if (!empty($fetchConditions)) {
            $whereStatement = " WHERE ";
            $conditions  = $whereStatement;
            
            foreach($fetchConditions as $tableColumn=>$value) {
                if ($conditions != $whereStatement) {
                    $conditions .= " AND ";
                }
                $conditions .= "`{$tableColumn}`='" . 
                        $connection->sanitize($value) . 
                        "' ";
            }
            
            $selectStatement .= $conditions;
        }
        
        $resultArray = $connection->getQueryResultAsArray($selectStatement);
        
        // handle non-existent zero-th element, according to code on pg 180 of
        // the book (for book details, see introductory comments at top of this file).
        if(!isset($resultArray[0])) {
            $resultArray[0] = array();
        }
        
        // store into internal state.
        foreach($resultArray as $index=>$innerArray) {
            $this->setValue($index, $innerArray);
        }
    }
}
?>